package com.zdxlz.fcmp.common.log.aop;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.URLUtil;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.zdxlz.fcmp.common.core.constant.enums.ClientTypeEnum;
import com.zdxlz.fcmp.common.core.constant.enums.RequestMethodEnum;
import com.zdxlz.fcmp.common.core.util.CommUtils;
import com.zdxlz.fcmp.common.log.annotation.LogRecord;
import com.zdxlz.fcmp.common.log.annotation.LoginLogRecord;
import com.zdxlz.fcmp.common.log.config.IpConfig;
import com.zdxlz.fcmp.common.security.util.UserLoginInfoUtil;
import com.zdxlz.fcmp.upms.api.entity.SystemExceptionLog;
import com.zdxlz.fcmp.upms.api.entity.SystemLoginLog;
import com.zdxlz.fcmp.upms.api.entity.SystemOperLog;
import com.zdxlz.fcmp.upms.api.entity.SystemUser;
import com.zdxlz.fcmp.upms.api.mapper.SystemUserMapper;
import com.zdxlz.fcmp.upms.api.service.ISystemExceptionLogService;
import com.zdxlz.fcmp.upms.api.service.ISystemLoginLogService;
import com.zdxlz.fcmp.upms.api.service.ISystemOperLogService;
import io.micrometer.common.util.StringUtils;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.expression.BeanFactoryAccessor;
import org.springframework.core.annotation.Order;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.*;

/**
 * @description：日志切面类
 * @author：aman
 * @date：2025/3/7 10:24
 */
@Aspect
@Component
@Order(1)
@Slf4j
public class LogAspect {

    public static final String LOGOUT = "/api/v1/sys/logout";

    // 设置相关请求白名单，不进行日志打印，且不写入操数据库
    private static final List<String> reqPathWhiteList = List.of("/api/v1/sys/captcha/enable", "/api/v1/sys/captcha/captcha", "/api/v1/sys/captcha/check");
    @Resource
    private IpConfig ipConfig;
    @Resource
    private SystemUserMapper systemUserMapper;
    @Autowired
    private ApplicationContext applicationContext;
    @Resource
    private ISystemLoginLogService systemLoginLogService;
    @Resource
    private ISystemExceptionLogService systemExceptionLogService;
    @Resource
    private ISystemOperLogService systemOperLogService;

    private SpelExpressionParser spelExpressionParser;

    @Bean
    public SpelExpressionParser initLogAspect() {
        return spelExpressionParser = new SpelExpressionParser();
    }

    /**
     * @description：定义日志切入点
     * @author：aman
     * @date：2025/3/7 10:25
     */
    @Pointcut("@annotation(com.zdxlz.fcmp.common.log.annotation.LogRecord) || @annotation(com.zdxlz.fcmp.common.log.annotation.LoginLogRecord) ")
    public void webLog() {
    }

    @Before("webLog()")
    public void doBefore(JoinPoint joinPoint) {
        //获取当前请求对象
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        String requestURI = request.getRequestURI();
        // 当前是退出系统操作，才进行以下处理
        if (LOGOUT.equals(requestURI))
            doLog(0, joinPoint, request, null, null);
    }

    /**
     * @description：日志环绕通知
     * @author：aman
     * @date：2025/3/7 10:25
     */
    @Around("webLog()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();
        //获取当前请求对象
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        // 当前不是退出系统操作，才进行以下处理
        if (LOGOUT.equals(request.getRequestURI())) return joinPoint.proceed();
        Object result = joinPoint.proceed();

        long endTime = System.currentTimeMillis();
        doLog(endTime - startTime, joinPoint, request, result, null);
        return result;
    }

    /**
     * @description：接口执行完成后，处理异常记录日志
     * @author：aman
     * @date：2025/3/7 16:42
     */
    @AfterThrowing(pointcut = "webLog()", throwing = "ex")
    public void exceptionLog(JoinPoint joinPoint, Exception ex) {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        doLog(0, joinPoint, request, null, ex);
    }

    /**
     * @description：处理请求日志
     * @author：aman
     * @date：2025/3/7 10:49
     */
    private void doLog(long time, JoinPoint joinPoint, HttpServletRequest request, Object result, Exception ex) {
        String module;
        Class<?> cls = joinPoint.getTarget().getClass();
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();
        String uriPath = URLUtil.url(request.getRequestURL().toString()).getPath();
        JSONObject reqJson = new JSONObject();
        if (!reqPathWhiteList.contains(uriPath)) {
            if (cls.isAnnotationPresent(LoginLogRecord.class)) {
                module = cls.getAnnotation(LoginLogRecord.class).module();
                LoginLogRecord loginLogRecord = method.getAnnotation(LoginLogRecord.class);
                if (loginLogRecord.db()) parseReqParameter(request, ex, reqJson, method);
                recordLoginLog(joinPoint, reqJson, result, ex, loginLogRecord, module, method);
            } else if (cls.isAnnotationPresent(LogRecord.class)) {
                module = cls.getAnnotation(LogRecord.class).module();
                LogRecord logRecord = method.getAnnotation(LogRecord.class);
                if (logRecord.db())  parseReqParameter(request, ex, reqJson, method);
                recordBusinessLog(time, joinPoint, reqJson, result, ex, logRecord, module, method);
            }
        } else {
            log.info("当前接口不进行日志操作，请求地址：{}", uriPath);
        }
    }

    /**
     * @description：解析并组装登陆日志信息
     * @author：aman
     * @date：2025/3/7 16:46
     */
    private void recordLoginLog(JoinPoint joinPoint, JSONObject reqJson, Object result, Exception ex, LoginLogRecord loginLogRecord, String module, Method method) {
        Map<String, Object> loginInfo = parsingLogInfo(joinPoint, loginLogRecord.value(), loginLogRecord.field(), method);
        String logInfo = loginInfo.get("resultInfo").toString();
        String requestObj = JSONObject.toJSONString(loginInfo.get("requestObj"));
        Integer loginType = loginLogRecord.loginType().getCode();
        // 进行解析异常操作
        String newModule = handleModule(module, loginLogRecord.module());
        if (ObjectUtil.isNotEmpty(ex)) {
            buildExceptionLogInfoAndInsert(newModule, loginType, requestObj, ex, reqJson, logInfo);
        } else {
            if (loginLogRecord.db()) {
                buildLoginLogInfoAndInsert(reqJson, result, loginType, logInfo);
            } else {
                log.info("系统操作日志打印===>系统模块：{}, 功能描述：{}, 页面查询参数：{}, 请求体参数：{}", newModule, logInfo, requestObj, reqJson);
            }
        }
    }

    /**
     * @description：解析并组装业务日志信息
     * @author：aman
     * @date：2025/3/7 16:50
     */
    private void recordBusinessLog(long time, JoinPoint joinPoint, JSONObject reqJson, Object result, Exception ex, LogRecord logRecord, String module, Method method) {
        Map<String, Object> loginInfo = parsingLogInfo(joinPoint, logRecord.value(), logRecord.field(), method);
        String logInfo = loginInfo.get("resultInfo").toString();
        String requestObj = JSONObject.toJSONString(loginInfo.get("requestObj"));
        // 进行解析异常操作
        String newModule = handleModule(module, logRecord.module());
        if (ObjectUtil.isNotEmpty(ex)) {
            // 获取详细方法上的详细模块
            buildExceptionLogInfoAndInsert(newModule, logRecord.operType().getCode(), requestObj, ex, reqJson, logInfo);
        } else {
            if (logRecord.db()) {
                buildOperLogAndInsert(time, newModule, reqJson, logRecord, requestObj, logInfo, result.toString());
            } else {
                log.info("系统操作日志打印===>系统模块：{}, 功能描述：{}, 页面查询参数：{}, 请求体参数：{}", newModule, logInfo, requestObj, reqJson);
            }
        }
    }

    /**
     * @Description：解析请求参数
     * @Auther：aman
     * @Date：2025/3/18 16:58
     */
    private void parseReqParameter(HttpServletRequest request, Exception ex, JSONObject reqJson, Method method) {
        String loginIp = CommUtils.getIpAddress(request);
        // 进行解析异常操作
        SystemUser userInfo = UserLoginInfoUtil.getUserInfo();
        if (ObjectUtil.isNotEmpty(ex)) {
            String msg = ex.getMessage();
            String mobile = msg.substring(msg.lastIndexOf("|") + 1);
            if (ObjectUtil.isEmpty(userInfo)) userInfo = systemUserMapper.selectOne(new LambdaUpdateWrapper<SystemUser>().eq(SystemUser::getMobile, mobile));
        }
        reqJson.put("tenantId", userInfo.getTenantId());
        reqJson.put("loginIp", loginIp);
        reqJson.put("loginUser", userInfo.getUsername());
        reqJson.put("loginMobile", userInfo.getMobile());
        reqJson.put("methodName", method.getName());
        reqJson.put("reqWay", request.getMethod());
        reqJson.put("reqUri", request.getRequestURI());
        JSONObject adressJson = ipConfig.underIpGetAdress(loginIp);
        reqJson.put("province", adressJson.getString("province"));
        reqJson.put("city", adressJson.getString("city"));
        reqJson.put("createBy", userInfo.getUsername());
        reqJson.put("createTime", new Date());
    }

    /**
     * @description：获取异常的堆栈信息
     * @author：aman
     * @date：2025/3/7 17:19
     */
    private static String getStackTrace(Exception ex) {
        StringBuilder stackTrace = new StringBuilder();
        for (StackTraceElement element : ex.getStackTrace()) {
            stackTrace.append(element.toString()).append("\n");
        }
        return stackTrace.toString();
    }

    private static String handleModule(String classModule, String subModule) {
        String newModule = "";
        if (StringUtils.isNotBlank(subModule)) {
            newModule = classModule.concat("【" + subModule + "】");
        }
        return newModule;
    }

    /**
     * @Description：获取所有字段集合
     * @Auther：aman
     * @Date：2025/3/19 10:35
     */
    private static List<String> getFieldList(Object parameter, String[] fields) {
        // 获取字段数据
        List<String> fieldsVal = new ArrayList<>();
        Map<String, Object> kvMap = new HashMap<>();
        if (parameter instanceof ArrayList valueArray) {
            fieldsVal.addAll(valueArray.stream().map(String::valueOf).toList());
        } else {
            if (null != parameter) {
                kvMap = parameter instanceof Map ? (Map<String, Object>) parameter : CommUtils.getObjectKVToMap(parameter);
            }
            for (String field : fields) {
                Object temp = kvMap.get(field);
                if (null != temp) {
                    String val = temp.toString();
                    fieldsVal.add(StringUtils.isNotBlank(val) ? val : "");
                } else {
                    fieldsVal.add("");
                }
            }
        }
        return fieldsVal;
    }

    // 分离 SpEL 表达式和普通占位符
    private String parseSpelExpression(JoinPoint joinPoint, String value) {
        StandardEvaluationContext context = new StandardEvaluationContext(applicationContext);
        context.addPropertyAccessor(new BeanFactoryAccessor());
        String[] parameterNames = ((MethodSignature) joinPoint.getSignature()).getParameterNames();
        for (int i = 0; i < parameterNames.length; i++) {
            context.setVariable(parameterNames[i], joinPoint.getArgs()[i]);
        }
        StringBuilder sb = new StringBuilder();
        int lastIndex = 0;
        int index = value.indexOf("#{");
        while (index != -1) {
            sb.append(value, lastIndex, index);
            int end = value.indexOf("}", index);
            String spelExpression = value.substring(index + 2, end);
            // 执行spel 表达式
            Expression expression = spelExpressionParser.parseExpression(spelExpression);
            String spelValue = expression.getValue(context).toString();
            sb.append(spelValue);
            lastIndex = index + 3 + spelExpression.length();
            index = value.indexOf("#{", lastIndex);
        }
        sb.append(value, lastIndex, value.length());
        return sb.toString();
    }

    private Map<String, Object> parsingLogInfo(JoinPoint joinPoint, String recordStr, String[] fields, Method method) {
        Map<String, Object> resultMap = new HashMap<>();
        // 解析完日志的spel表达式
        String info = parseSpelExpression(joinPoint, recordStr);
        Object parameter = getParameter(method, joinPoint.getArgs());
        List<String> fieldsVal = getFieldList(parameter, fields);
        String resultInfo = CommUtils.replaceStrPlaceholder(info, fieldsVal);
        resultMap.put("fieldsVal", fieldsVal.toString());
        resultMap.put("resultInfo", resultInfo);
        resultMap.put("requestObj", parameter);
        return resultMap;
    }

    /**
     * 获取切点方法的参数
     *
     * @param method 切点方法
     * @param args   参数
     * @return 参数集合对象
     */
    private Object getParameter(Method method, Object[] args) {
        List<Object> argList = new ArrayList<>();
        Map<String, Object> map = new HashMap<>();
        Parameter[] parameters = method.getParameters();
        for (int i = 0; i < parameters.length; i++) {
            //将RequestBody注解修饰的参数作为请求参数
            RequestBody requestBody = parameters[i].getAnnotation(RequestBody.class);
            if (requestBody != null) {
                argList.add(args[i]);
            }
            //将RequestParam注解修饰的参数作为请求参数
            RequestParam requestParam = parameters[i].getAnnotation(RequestParam.class);
            if (requestParam != null) {
                String key = parameters[i].getName();
                if (!StringUtils.isEmpty(requestParam.value())) {
                    key = requestParam.value();
                }
                map.put(key, args[i]);
            }
            //将【ParameterObject】注解修饰的参数作为请求参数
            ParameterObject parameterObject = parameters[i].getAnnotation(ParameterObject.class);
            if (parameterObject != null) {
                String key = parameters[i].getName();
                if (!"page".equals(key)) {
                    argList.add(args[i]);
                }
            }
        }
        if (!map.isEmpty()) {
            argList.add(map);
        }
        if (argList.isEmpty()) {
            return null;
        } else if (argList.size() == 1) {
            return argList.get(0);
        } else {
            return argList;
        }
    }

    /**
     * @Description：构建登陆日志并入库
     * @Auther：aman
     * @Date：2025/3/18 09:31
     */
    private void buildLoginLogInfoAndInsert(JSONObject reqJson, Object result, Integer loginType, String logInfo) {
        SystemLoginLog loginLog = new SystemLoginLog();
        loginLog.setTenantId(reqJson.getInteger("tenantId"));
        loginLog.setLoginType(loginType);
        loginLog.setLoginIp(reqJson.getString("loginIp"));
        loginLog.setLoginAddress(reqJson.getString("province") + "-" + reqJson.getString("city"));
        loginLog.setLoginName(reqJson.getString("loginUser"));
        loginLog.setLoginMobile(reqJson.getString("loginMobile"));
        loginLog.setClientType(ClientTypeEnum.COMPUTER.getCode());
        if (ObjectUtil.isNotEmpty(result)) {
            String resultString = result.toString();
            String concat = logInfo.concat("- ").concat(resultString.substring(resultString.indexOf("msg=") + 4, resultString.lastIndexOf(", data")));
            loginLog.setLoginMsg(concat);
        } else {
            loginLog.setLoginMsg(logInfo.replace("【】", "【" + reqJson.getString("loginMobile")) + "】- 退出系统成功！");
        }
        log.info("用户【登陆】或【登出】日志记录，loginLog:{}", JSONObject.toJSONString(loginLog));
        systemLoginLogService.insertSystemLoginLog(loginLog);
    }

    /**
     * @Description：构建操作日志并入库
     * @Auther：aman
     * @Date：2025/3/18 09:31
     */
    private void buildOperLogAndInsert(long time, String module, JSONObject reqJson, LogRecord logRecord, String requestObj, String logInfo, String result) {
        SystemOperLog systemOperLog = new SystemOperLog();
        systemOperLog.setTenantId(reqJson.getInteger("tenantId"));
        systemOperLog.setTitle(module);
        systemOperLog.setOperMsg(logInfo);
        systemOperLog.setOperType(logRecord.operType().getCode());
        systemOperLog.setOperIp(reqJson.getString("loginIp"));
        systemOperLog.setOperLocation(reqJson.getString("province") + "-" + reqJson.getString("city"));
        systemOperLog.setReqUrl(reqJson.getString("reqUri"));
        systemOperLog.setReqMethod(reqJson.getString("methodName"));
        systemOperLog.setReqParam(requestObj);
        systemOperLog.setRespResult(result);
        systemOperLog.setReqConsuming((int) time);
        systemOperLog.setOperTime(new Date());
        systemOperLog.setOperName(reqJson.getString("loginUser"));
        systemOperLog.setOperMobile(reqJson.getString("loginMobile"));
        systemOperLog.setReqWay(RequestMethodEnum.valueOf(reqJson.getString("reqWay")).getCode());
        systemOperLog.setClientType(ClientTypeEnum.COMPUTER.getCode());

        log.info("用户【操作】日志记录，systemOperLog:{}", JSONObject.toJSONString(systemOperLog));
        systemOperLogService.insertSystemOperLog(systemOperLog);
    }

    /**
     * @description：构建异常日志并入库
     * @author：aman
     * @date：2025/3/7 16:50
     */
    private void buildExceptionLogInfoAndInsert(String module, Object operType, String requestObj, Exception ex, JSONObject reqJson, String logInfo) {
        SystemExceptionLog exceptionLog = new SystemExceptionLog();
        exceptionLog.setTenantId(reqJson.getInteger("tenantId"));
        exceptionLog.setExcpName(reqJson.getString("loginUser"));
        exceptionLog.setExcpMobile(reqJson.getString("loginMobile"));
        exceptionLog.setExcpModule(module);
        exceptionLog.setExcpMsg(logInfo);
        exceptionLog.setBusinessType((Integer) operType);
        exceptionLog.setExcpIp(reqJson.getString("loginIp"));
        exceptionLog.setReqMethod(reqJson.getString("reqUri").concat(".").concat(reqJson.getString("methodName")));
        exceptionLog.setReqParam(requestObj);
        exceptionLog.setClientType(ClientTypeEnum.COMPUTER.getCode());
        exceptionLog.setErrorMsg(ex.toString() + "\n" + getStackTrace(ex));
        exceptionLog.setCreateBy(reqJson.getString("createBy"));
        exceptionLog.setCreateTime(DateUtil.parseDate(reqJson.getString("createTime")).toLocalDateTime());
        exceptionLog.setUpdateBy(reqJson.getString("createBy"));

        log.info("用户【异常】日志记录，exceptionLog:{}", JSONObject.toJSONString(exceptionLog));
        systemExceptionLogService.insertSystemExceptionLog(exceptionLog);
    }
}
